﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using SSharp.Core.DataTypes;
using System.Numerics;

namespace SSharp.Core.Reader {
	/// <summary>
	/// Will accept an input stream and produce a DataStream of scheme objects.
	/// </summary>
	public class SchemeReader : IDisposable {
		private CharReaderBufferer reader;

		public SchemeReader(Stream stream) {
			reader = new CharReaderBufferer(stream);
		}

		/// <summary>
		/// Reads a Scheme expression.
		/// Returns null at end of stream.
		/// </summary>
		public object ReadExpression() {
			return ReadExpression(null);
		}

		/// <summary>
		/// Returns an enumeration of every object in the file; this will produce the same results as calling ReadExpression() repeatedly.
		/// </summary>
		public IEnumerable<object> ReadAll() {
			while (true) {
				object obj = ReadExpression();
				if (obj == null) {
					yield break;
				}
				yield return obj;
			}
		}

		/// <summary>
		/// Reads a Scheme expression; if terminator is not null, it is the character expected for "no more expressions" and null should be returned when it is read.
		/// Returns null at end of stream if and only if terminator is null - otherwise it is an error.
		/// 
		/// For example, after reading a '(', ReadExpression should be called with a ')' terminator.
		/// </summary>
		private object ReadExpression(char? terminator) {
			char? c = reader.ReadChar();
			if (c == terminator) {
				// terminator found (or end-of-stream if terminator is null)
				return null;
			}

			if (c == null) {
				// c == null means end-of-stream. Since c != terminator that means we're not expecting the end of stream!
				throw new ReaderException(String.Format("Expected {0}, found <end of stream>", terminator));
			}

			if (Characters.IsIllegal(c)) {
				throw new ReaderException("Invalid character " + c);
			}

			char? match = Characters.GetParenthesisMatch(c.Value);
			if (match != null) {
				return ReadList(match);
			}

			if (Characters.IsWhitespace(c.Value)) {
				// ignore whitespace at this point.
				return ReadExpression(terminator);
			}

			string quote = Characters.GetQuote(c.Value);
			if (quote != null) {
				return ReadQuote(quote);
			}

			if (c == '\"') {
				return ReadString();
			}

			if (c == ';') {
				ReadComment();
				return ReadExpression(terminator);
			}

			// all special characters have been handled - assume this character is a symbol/number (they are read the same).
			return ReadSymbol(c.Value);
		}

		/// <summary>
		/// Reads a comment.
		/// </summary>
		private void ReadComment() {
			while (true) {
				char? c = reader.ReadChar();
				if (c == null) {
					return;
				}
				if (c == '\n') {
					// keep the newline; we need a separating character anyway
					reader.UnreadChar('\n');
					return;
				}
			}
		}

		/// <summary>
		/// Reads a symbol or number.
		/// The reason the same method reads symbols and numbers it that numbers are indistinguishible from symbols without
		/// the full number, for example 1923872.2+198i is a number but 1923872.2+198ij isn't.
		/// </summary>
		private object ReadSymbol(char firstChar) {
			StringBuilder result = new StringBuilder();
			result.Append(firstChar);

			while (true) {
				char? next = reader.ReadChar();
				if (next == null) {
					break;
				}

				if (Characters.IsSymbolTerminator(next.Value)) {
					reader.UnreadChar(next.Value);
					break;
				}

				result.Append(next.Value);
			}

			return StringToSymbol(result.ToString());
		}

		/// <summary>
		/// Returns a symbol (or number) given the string representation.
		/// </summary>
		private object StringToSymbol(string str) {
			// check if the string is a number.
			object number = StringToNumber(str);
			if (number != null) {
				return number;
			} else {
				return new Symbol(str);
			}
		}

		/// <summary>
		/// Tries to parse a number from the string, and returns the result or null.
		/// </summary>
		private object StringToNumber(string str) {
			bool hasDecimalPoint = false;

			// first, check that the string represents a number
			foreach (char c in str) {
				if (c == '.') {
					if (hasDecimalPoint) {
						// two decimal points?
						return null;
					} else {
						hasDecimalPoint = true;
					}
				} else if (!char.IsDigit(c)) {
					// character is neither a . nor a digit; this is not a valid number.
					return null;
				}
			}

			if (hasDecimalPoint) {
				// the number has a decimal point; we parse it as a decimal. If it's too big (or too small) an exception will occur.
				return decimal.Parse(str);
			} else {
				// the number doesn't have a decimal point; we parse it as a BigInteger.
				return BigInteger.Parse(str);
			}
		}

		/// <summary>
		/// Called after a quote (or quote-like) character is called.
		/// Basically returns a list with the quote-string and the next read expression
		/// </summary>
		private object ReadQuote(string quote) {
			object value = ReadExpression(null);

			if (value == null) {
				throw new ReaderException("Quote must be followed by an expression.");
			}

			return List.Create(
				new Symbol(quote),
				// I can send null as terminator, because the quote will be over when ReadExpression returns.
				value
			);
		}

		private object ReadList(char? terminator) {
			List<object> result = new List<object>();

			while (true) {
				// read expressions until the terminator is found.
				object value = ReadExpression(terminator);
				if (value == null) {
					break;
				}
				result.Add(value);
			}

			return List.Create(result.ToArray());
		}

		private string ReadString() {
			StringBuilder result = new StringBuilder();

			while (true) {
				char? next = reader.ReadChar();

				if (next == null)
					throw new ReaderException("End of stream in string constant");

				if (next.Value == '\"')
					break;

				if (next.Value == '\\') {
					next = reader.ReadChar();
					if (next == null)
						throw new ReaderException("End of stream in string constant");
					result.Append(UnescapeInString(next.Value));
				} else {
					result.Append(next.Value);
				}
			}

			return result.ToString();
		}

		/// <summary>
		/// Called with an escape character. E.g. when \n appears in a string, the parameter for this function is 'n'.
		/// </summary>
		private string UnescapeInString(char p) {
			switch (p) {
				case 'n':
					return "\n";
				case 'r':
					return "\r";
				case '\\':
					return "\\";
				case '\"':
					return "\"";

				default:
					throw new ReaderException("Unknown escape sequence \\" + p);
			}
		}

		public void Dispose() {
			reader.Dispose();
		}

		/// <summary>
		/// Contains all the (static) utility-style methods for understanding characters.
		/// </summary>
		private static class Characters {
			private static Dictionary<char, char> parenthesis = new Dictionary<char, char> {
				{'(', ')'},
				{'[', ']'},
				{'{', '}'},
			};

			private static HashSet<char> otherTerminators = new HashSet<char> {
				'\"', ';'
			};

			private static HashSet<char> illegals = new HashSet<char> {
				')', ']', '}' // these are illegal except where they're expected
			};

			private static Dictionary<char, string> quotes = new Dictionary<char, string> {
				{'\'', "quote"},
				{'`', "quasiquote"},
				{',', "unquote"}
			};

			/// <summary>
			/// If the given char is a parenthesis opener ( [ or {, returns its match ) ] or } respectively.
			/// Otherwise, returns null.
			/// </summary>
			public static Nullable<char> GetParenthesisMatch(char? c) {
				char result;
				if (parenthesis.TryGetValue(c.Value, out result))
					return result;
				else
					return null;
			}

			public static bool IsIllegal(char? c) {
				return illegals.Contains(c.Value);
			}

			/// <summary>
			/// Checks if the given character is a quote-type character and returns it, or returns null if the character is not a quote character.
			/// </summary>
			public static string GetQuote(char c) {
				string result;
				quotes.TryGetValue(c, out result);
				return result;
			}

			/// <summary>
			/// Returns true if the given character is considered whitespace.
			/// </summary>
			public static bool IsWhitespace(char c) {
				return char.IsWhiteSpace(c);
			}

			/// <summary>
			/// Returns true if the given character would terminate a symbol.
			/// </summary>
			public static bool IsSymbolTerminator(char c) {
				if (IsWhitespace(c)) {
					return true;
				}

				if (parenthesis.ContainsKey(c) || parenthesis.ContainsValue(c)) {
					return true;
				}

				if (quotes.ContainsKey(c)) {
					return true;
				}

				if (otherTerminators.Contains(c)) {
					return true;
				}

				return false;
			}
		}
	}
}
